﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;

namespace Mindfor
{
	/// <summary>
	/// Provides extension functions for different types.
	/// </summary>
	public static class Extensions
	{
		#region IEnumerable

		/// <summary>
		/// Merges elements to string with specified delimeter.
		/// </summary>
		/// <param name="e">Elements to merge.</param>
		/// <param name="delimeter">Delimeter between elements.</param>
		/// <returns>Merged string.</returns>
		public static string Merge(this IEnumerable<string> e, string delimeter)
		{
			StringBuilder sb = new StringBuilder();
			foreach (string s in e)
			{
				if (sb.Length > 0)
					sb.Append(delimeter);
				sb.Append(s);
			}
			return sb.ToString();
		}

		#endregion

		#region Object

		/// <summary>
		/// Try cast <paramref name="obj"/> value to type <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T">Type to convert to.</typeparam>
		/// <param name="obj">Object to convert.</param>
		public static T ToOrException<T>(this object obj)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");
			if (obj is T)
				return (T)obj;

			// check string & enum
			Type type = typeof(T);
			if (type == typeof(String))
				return (T)(object)obj.ToString();

			// check nullable
			Type underlyingType = Nullable.GetUnderlyingType(type);
			if (underlyingType != null)
				type = underlyingType;

			// check for enum
			if (type.IsEnum)
			{
				if (type.IsEnumDefined(obj))
					return (T)Enum.Parse(type, obj.ToString());
				throw new InvalidCastException();
			}

			// standart converter
			return (T)Convert.ChangeType(obj, type);
		}

		/// <summary>
		/// Try cast <paramref name="obj"/> value to type <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T">Type to convert to.</typeparam>
		/// <param name="obj">Object to convert.</param>
		/// <param name="defaultValue">The value to return if conversion is failed.</param>
		public static T To<T>(this object obj, T defaultValue = default(T))
		{
			if (obj == null)
				return defaultValue;
			if (obj is T)
				return (T)obj;

			// check string & enum
			Type type = typeof(T);
			if (type == typeof(String))
				return (T)(object)obj.ToString();

			// check nullable
			Type underlyingType = Nullable.GetUnderlyingType(type);
			if (underlyingType != null)
				type = underlyingType;

			// check for enum
			if (type.IsEnum)
			{
				if (type.IsEnumDefined(obj))
					return (T)Enum.Parse(type, obj.ToString());
				return defaultValue;
			}

			// standart converter
			try
			{
				return (T)Convert.ChangeType(obj, type);
			}
			catch
			{
				return defaultValue;
			}
		}

		/// <summary>
		/// Returns object property value. If property does not exits then <c>Null</c>.
		/// </summary>
		/// <param name="obj">Object to get property from.</param>
		/// <param name="property">Property name.</param>
		/// <returns>Property value or <c>Null</c> if not found.</returns>
		public static object GetPropertyValue(this object obj, string property)
		{
			PropertyDescriptor prop = TypeDescriptor.GetProperties(obj).Find(property, false);
			if (prop != null)
				return prop.GetValue(obj);
			return null;
		}

		/// <summary>
		/// Returns object property typed value. If property does not exits then default value.
		/// </summary>
		/// <param name="obj">Object to get property from.</param>
		/// <param name="property">Property name.</param>
		/// <returns>Property typed value or default value if not found.</returns>
		public static T GetPropertyValue<T>(this object obj, string property)
		{
			return GetPropertyValue(obj, property).To<T>();
		}

		/// <summary>
		/// Copies source property values to target matching property.
		/// </summary>
		/// <param name="source">Source object to get field values from.</param>
		/// <param name="target">target object to set field values.</param>
		/// <param name="checkPropertyTypes">If true, then copies only properties with equal types (other omitted).
		/// If false, throws exception when properties have different types.</param>
		/// <param name="exclude">Properties to exclude from copy list.</param>
		public static void CopyPropertyValuesTo<T1, T2>(this T1 source, T2 target, bool checkPropertyTypes = true, params string[] exclude)
		{
			// get proeprties
			PropertyDescriptorCollection sourceProps = TypeDescriptor.GetProperties(source);
			PropertyDescriptorCollection targetProps = TypeDescriptor.GetProperties(target);
			
			// set target properties that matches their name to source object properties
			foreach (PropertyDescriptor targetProp in targetProps)
			{
				if (!exclude.Contains(targetProp.Name) && !targetProp.IsReadOnly)
				{
					PropertyDescriptor sourceProp = sourceProps.Find(targetProp.Name, false);
					if (sourceProp != null && 
						(!checkPropertyTypes || sourceProp.PropertyType == targetProp.PropertyType))
						targetProp.SetValue(target, sourceProp.GetValue(source));
				}
			}
		}

		#endregion

		#region IDictionary

		/// <summary>
		/// Adds elements to dictionary from object. Key = property name, Value = property value.
		/// </summary>
		/// <param name="dic">Dictionary to add to.</param>
		/// <param name="values">Object to get properties from.</param>
		public static void Add(this IDictionary<string, object> dic, object values)
		{
			if (values != null)
			{
				foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(values))
				{
					object val = prop.GetValue(values);
					dic[prop.Name] = val;
				}
			}
		}

		/// <summary>
		/// Returns value of <typeparam name="TValue"/> type from dictionary by key.
		/// If not found return defaulTValue of <typeparam name="TValue"/>.
		/// </summary>
		/// <param name="dic">Dictionary.</param>
		/// <param name="key">Key to search.</param>
		/// <param name="defaultValue">DefaultValue to return if value is not found or can not be cast.</param>
		public static TValue Get<TKey, TValue>(this IDictionary<TKey, TValue> dic, TKey key, TValue defaultValue = default(TValue))
		{
			TValue val;
			if (dic.TryGetValue(key, out val))
				return val;
			return defaultValue;
		}

		#endregion

		#region DateTime

		/// <summary>
		/// Converts DateTime value to short date and time string.
		/// </summary>
		/// <param name="dateTime">DateTime to convert.</param>
		public static string ToShortDateTimeString(this DateTime dateTime)
		{
			return dateTime.ToShortDateString() + " " + dateTime.ToShortTimeString();
		}

		#endregion

		#region String

		/// <summary>
		/// Duplicates string n-times.
		/// </summary>
		/// <param name="str">String to duplicate.</param>
		/// <param name="n">Times to duplicate.</param>
		public static string Multiply(this string str, int n)
		{
			if (n < 0)
				throw new ArgumentException("N must zero or positive.", "n");

			StringBuilder sb = new StringBuilder();
			while (n > 0)
			{
				sb.Append(str);
				n--;
			}
			return sb.ToString();
		}

		#endregion
	}
}
